package com.tools.web.interceptor.auth;

import com.tools.common.container.r.OnlyReadHashMap;
import com.tools.common.container.r.OnlyReadHashSet;
import com.tools.common.exception.DataGenerationException;
import com.tools.common.object.Note;
import com.tools.common.thread.TimePair;
import com.tools.common.thread.Timeunit;
import com.tools.web.WebKit;
import com.tools.web.http.HttpDataFromEnum;
import com.tools.web.interceptor.access.AccessManager;
import com.tools.web.interceptor.access.AccessSource;
import com.tools.web.interceptor.access.own.DefaultAccessManager;
import com.tools.web.interceptor.access.spel.AuthorizeManager;
import org.slf4j.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * 通用的身份认证拦截器的抽象实现
 * */
@Note("通用的身份认证拦截器的抽象实现")
public abstract class GenericAuthenticationManager implements AuthenticationManager {


    @Note("AuthToken 位于 HTTP 请求的哪个地方")
    private final HttpDataFromEnum tokenIn;

    @Note("AuthToken 的名称")
    private final String tokenName;

    @Note("缓存 AuthUser 对象时的 key 的前缀")
    private final String authUserSaveKeyPrefix;

    @Note("【异常 JSON 信息】: 没有获取到 AuthToken")
    private final String tokenNotFoundErrorJson;

    @Note("【异常 JSON 信息】: AuthToken 解析失败或者已过期")
    private final String tokenParseFailedErrorJson;

    @Note("【异常 JSON 信息】: AuthToken 已过期")
    private final String tokenExpiredErrorJson;

    @Note("AuthToken 的续期工具实例")
    private final AuthTokenExtender authTokenExtender;

    @Note("加载和操作 AuthUser 实例对象的业务操作接口")
    private final AuthUserService authUserService;

    @Note("在身份认证拦截器里开启授权功能，时就会启动此 [授权拦截器]")
    private final AccessManager accessManager;

    @Note("设备类型集合")
    private final Set<String> loginDeviceTypeSet;

    @Note("设备类型与最大在线总数的映射 Map")
    private final Map<String, Integer> loginDeviceTypeAndMaxOnlineTotal;

    @Note("平台禁用时，平台代表的唯一 ID")
    private final Object disabledPlatformId;

    @Note("缓存 AuthUser 对象的 key 的各部分的分隔符")
    public static final char KEY_SEPARATOR = ':';

    /* **************************************************************************************
     *
     *           构造器
     *
     * **************************************************************************************
     * */

    protected GenericAuthenticationManager(AuthSource source, AuthUserService authUserService) {
        if(source == null) throw new NullPointerException("AuthSource 配置源对象不能为 null");
        this.tokenIn = source.getTokenIn();
        this.tokenName = source.getTokenName();
        this.authUserSaveKeyPrefix = source.getAuthUserSaveKeyPrefix();
        this.tokenNotFoundErrorJson = source.getTokenNotFoundErrorJson();
        this.tokenParseFailedErrorJson = source.getTokenParseFailedErrorJson();
        this.tokenExpiredErrorJson = source.getTokenExpiredErrorJson();
        this.authUserService = authUserService;
        this.disabledPlatformId = source.getDisabledPlatformId();
        this.authTokenExtender = source.getAuthTokenExtender();
        this.accessManager = this.private_buildAccessManager(source);
        Map<String, Integer> loginDeviceTypeAndMaxOnlineTotal = source.getLoginDeviceTypeAndMaxOnlineTotal();
        if(loginDeviceTypeAndMaxOnlineTotal != null && !loginDeviceTypeAndMaxOnlineTotal.isEmpty()) {
            this.loginDeviceTypeAndMaxOnlineTotal = new OnlyReadHashMap<>(loginDeviceTypeAndMaxOnlineTotal);
        } else {
            this.loginDeviceTypeAndMaxOnlineTotal = null;
        }
        this.loginDeviceTypeSet = new OnlyReadHashSet<>(source.getLoginDeviceTypes());
    }


    /* **************************************************************************************
     *
     *           核心拦截功能
     *
     * **************************************************************************************
     * */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String encryptedToken = this.tokenIn.getData(request, this.tokenName);
        if(encryptedToken == null || encryptedToken.isEmpty()) {
            response.getWriter().write(this.tokenNotFoundErrorJson);
            return false;
        }
        Logger logger = this.protected_getLogger();
        //解密为 AuthToken 实例
        AuthToken decryptedAuthToken;
        try {
            decryptedAuthToken = this.decryptAuthToken(encryptedToken);
        } catch (Exception e) {
            logger.info("token: {} 解析失败, 原因是: {}", encryptedToken, e.getMessage());
            response.getWriter().write(this.tokenParseFailedErrorJson);
            return false;
        }
        //获取用户认证对象
        String key = this.getAuthUserSaveKey(decryptedAuthToken);
        GetAuthUserResult result = this.getAuthUser(key, decryptedAuthToken);
        //续期或者放行下一步
        return this.private_extendAuthTokenLifeOrBlockForward(result, handler,
                request, response, encryptedToken,
                decryptedAuthToken, key, logger);
    }



    /* **************************************************************************************
     *
     *           需要子类实现的方法
     *
     * **************************************************************************************
     * */

    @Note("获取子类实现的日志对象")
    protected abstract Logger protected_getLogger();

    @Note("从缓存中获取 AuthUser 对象")
    protected abstract GetAuthUserResult getAuthUser(String key, AuthToken decryptedAuthToken);

    @Note("将新的 AuthUser 对象 put 进缓存中并设置过期时间为 token 的有效期，如果缓存中已存在则会替换掉")
    protected abstract void setAuthUser(String key, AuthUser srcAuthUser, AuthToken decryptedAuthToken);

    @Note("将指定的用户过期")
    protected abstract void removeAuthUser(String key);

    @Note("更新用户数据对象")
    protected abstract void updateAuthUser(String key, AuthUser authUser);

    @Note("子类实现的具体登出逻辑")
    protected abstract void protected_loginOut(String key, String loginDeviceType, String uuid);


    @Note("登录操作逻辑")
    @Override
    public final void handleLogin(Object userId, String loginUserType, String loginDeviceType, Object user,
                                  long expiration, Timeunit timeunit, HttpServletRequest request,
                                  HttpServletResponse response) {
        long expirationMill = Timeunit.toMill(expiration, timeunit);
        long now = System.currentTimeMillis();
        AuthToken accessAuthToken = this.newEmptyAuthToken();
        accessAuthToken.setUuid(UUID.randomUUID().toString().replace("-", ""));
        accessAuthToken.setUserId(userId);
        accessAuthToken.setLoginUserType(loginUserType);
        accessAuthToken.setLoginDeviceType(loginDeviceType);
        accessAuthToken.setLoginTimeMill(now, Timeunit.MILL);
        accessAuthToken.setExtendedCount(0);
        accessAuthToken.setExpirationMill(now + expirationMill, Timeunit.MILL);
        //加密 token 并设置到响应头里，开放给客户端
        String encryptedAccessAuthToken = accessAuthToken.encrypt();
        this.tokenIn.setData(request, response, this.tokenName, encryptedAccessAuthToken);
        WebKit.openHeader(response, this.tokenName);
        //看看当前是否开启了续期，且续期方式是否是双 token 机制，是的话就将 refresh_token 也加入返回序列中
        if(this.isRefreshTokenExtender() && this.authTokenExtender.isEnable()) {
            AuthToken refreshAuthToken = this.newRefreshAuthToken(accessAuthToken);
            String encryptedRefreshAuthToken = refreshAuthToken.encrypt();
            RefreshAuthTokenExtender extender = (RefreshAuthTokenExtender) this.authTokenExtender;
            String refreshTokenName = extender.getRefreshTokenName();
            extender.getRefreshTokenIn().setData(request, response, refreshTokenName, encryptedRefreshAuthToken);
            WebKit.openHeader(response, refreshTokenName);
        }
        //将用户对象和加载处理的授权数据一并组合成 AuthUser 对象，然后放入缓存中
        AuthUser authUser = this.authUserService.loadAuthUser(user, userId, loginUserType, loginDeviceType);
        authUser.setLoginDeviceType(loginDeviceType);
        String key = this.getAuthUserSaveKey(userId, loginUserType);
        this.setAuthUser(key, authUser, accessAuthToken);
    }

    @Override
    public void handleLogin(Object userId, String loginUserType, String loginDeviceType, Object user, TimePair expiration, HttpServletRequest request, HttpServletResponse response) {
        this.handleLogin(userId, loginUserType, loginDeviceType, user,
                expiration.getSourceTime(), expiration.getSourceTimeunit(),
                request, response);
    }

    @Note("根据 access_token 构造 refresh_token")
    public final AuthToken newRefreshAuthToken(AuthToken accessToken) {
        AuthToken refreshAuthToken = this.newEmptyAuthToken();
        refreshAuthToken.setUuid(accessToken.getUuid());
        refreshAuthToken.setUserId(accessToken.getUserId());
        refreshAuthToken.setLoginUserType(accessToken.getLoginUserType());
        refreshAuthToken.setLoginDeviceType(accessToken.getLoginDeviceType());
        refreshAuthToken.setLoginTimeMill(accessToken.getLoginTimeMill(), Timeunit.MILL);
        refreshAuthToken.setExtendedCount(this.authTokenExtender.getMaxAllowExtendCount());
        long expirationMill = accessToken.getExpirationMill() + this.authTokenExtender.getMaxAllowExtendTimeMill();
        refreshAuthToken.setExpirationMill(expirationMill, Timeunit.MILL);
        return refreshAuthToken;
    }

    @Override
    @Note("封禁用户操作")
    public final void handleDisable(HttpServletRequest request) {
        String encryptedToken = this.tokenIn.getData(request, this.tokenName);
        if(encryptedToken == null || encryptedToken.isEmpty()) return;
        AuthToken decryptedToken;
        try {
            decryptedToken = this.decryptAuthToken(encryptedToken);
        } catch (Exception e) {
            this.protected_getLogger().error("【封禁操作】：解密 token [{}] 为 AuthToken 实例失败，暂无法执行封禁操作，失败信息: {}",
                    encryptedToken, e.getMessage());
            return;
        }
        Object userId = decryptedToken.getUserId();
        String loginUserType = decryptedToken.getLoginUserType();
        String key = this.getAuthUserSaveKey(userId, loginUserType);
        //移除缓存，防止再有请求过来
        this.removeAuthUser(key);
        Logger logger = this.protected_getLogger();
        //封禁操作
        if(this.authUserService == null) {
            logger.info("并未初始化 AuthUserService，数据库封禁操作取消。 ID-loginUserType：【{}】 【{}】", userId, loginUserType);
            return;
        }
        int disabledResult = this.authUserService.disabledUser(userId, loginUserType, this.disabledPlatformId);
        if(disabledResult > 0) {
            logger.info("平台 {} 封禁 ID-loginUserType 为 【{}】 【{}】的用户/账户成功!", this.disabledPlatformId, userId, loginUserType);
        } else {
            logger.info("平台 {} 封禁 ID-loginUserType 为 【{}】 【{}】 的用户/账户失败，请查看 SQL 日志以检查修改失败的原因!", this.disabledPlatformId, userId, loginUserType);
        }
    }


    @Note("退出登录的操作")
    @Override
    public final void handleLoginOut(HttpServletRequest request) {
        AuthUser authUser = (AuthUser) request.getAttribute(AUTH_USER_NAME);
        String key = this.getAuthUserSaveKey(authUser.getUserId(), authUser.getLoginUserType());
        String deviceTypeId = authUser.getLoginDeviceType();
        String uuid = authUser.getUuid();
        this.protected_loginOut(key, deviceTypeId, uuid);
    }


    @Override
    @Note("更新 “用户数据/角色/权限/路由/自定义数据” 等操作时的后续同步更新操作")
    public final void handleUpdate(Object userId, String loginUserType, String loginDeviceType) {
        AuthUser newAuthUser = this.authUserService.loadAuthUser(userId, loginUserType, loginDeviceType);
        this.handleUpdate(newAuthUser);
    }

    @Override
    @Note("更新 “用户数据/角色/权限/路由/自定义数据” 等操作时的后续同步更新操作")
    public final void handleUpdate(AuthUser newAuthUser) {
        Object userId = newAuthUser.getUserId();
        String loginUserType = newAuthUser.getLoginUserType();
        String key = this.getAuthUserSaveKey(userId, loginUserType);
        this.updateAuthUser(key, newAuthUser);
    }


    /* **************************************************************************************
     *
     *           实现接口的方法
     *
     * **************************************************************************************
     * */

    @Override
    public final String getAuthUserSaveKey(AuthToken decryptedToken) {
        return this.getAuthUserSaveKey(decryptedToken.getUserId(), decryptedToken.getLoginUserType());
    }

    @Override
    public final String getAuthUserSaveKey(Object userId, String loginUserType) {
        return this.authUserSaveKeyPrefix + userId
                + private_handleStringKeyPrefix(loginUserType);
    }

    @Override
    public final String getMultipleAuthUserSaveKey(String sourceKey) {
        return sourceKey + KEY_SEPARATOR + AUTH_USER_NAME;
    }

    @Override
    public final String getMultipleDeviceSavePrefix(String sourceKey, String loginDeviceType) {
        return sourceKey + KEY_SEPARATOR + loginDeviceType;
    }

    @Override
    public final String getMultipleLoginRecordSaveKey(String sourceKey, String loginDeviceType,
                                                                   String uuid) {
        return this.getMultipleDeviceSavePrefix(sourceKey, loginDeviceType) + KEY_SEPARATOR + uuid;
    }

    @Override
    public final AuthUserService getAuthUserService() {
        return authUserService;
    }

    @Override
    public final AuthTokenExtender getAuthTokenExtender() {
        return authTokenExtender;
    }

    @Override
    public final boolean isRefreshTokenExtender() {
        return authTokenExtender instanceof RefreshAuthTokenExtender;
    }

    @Override
    public final HttpDataFromEnum getTokenIn() {
        return tokenIn;
    }

    @Override
    public final String getTokenName() {
        return tokenName;
    }

    @Override
    public final Object getDisabledPlatformId() {
        return disabledPlatformId;
    }

    @Override
    public final Map<String, Integer> getLoginDeviceTypeAndMaxOnlineTotal() {
        return loginDeviceTypeAndMaxOnlineTotal;
    }

    @Note("判断 token 是否已过期，是则返回允许续期的结果，否则不允许续期")
    protected final GetAuthUserResult protected_getResultIfExpired(AuthToken decryptedAuthToken) {
        return decryptedAuthToken.isExpired()
                ? new GetAuthUserResult(true, null)
                : new GetAuthUserResult(false, null);
    }


    @Note("检查配置的设备类型 ID 和允许最大在线数量是否正确")
    protected final void protected_checkLoginDeviceTypeAndMaxOnlineTotalMap() {
        if(this.loginDeviceTypeAndMaxOnlineTotal == null || this.loginDeviceTypeAndMaxOnlineTotal.isEmpty()) return;
        for (Map.Entry<String, Integer> e : this.loginDeviceTypeAndMaxOnlineTotal.entrySet()) {
            String loginDeviceType = e.getKey();
            Integer maxCount = e.getValue();
            if(!this.loginDeviceTypeSet.contains(loginDeviceType)) {
                throw new IllegalArgumentException("不知道 " + loginDeviceType + " 是什么设备类型");
            }
            if(maxCount == null) {
                throw new IllegalArgumentException(loginDeviceType + " 的最大在线设备数量为 null");
            }
            if(maxCount < 0) {
                throw new IllegalArgumentException(loginDeviceType + " 的最大在线设备数量为负数");
            }
        }
    }

    /* **************************************************************************************
     *
     *           私有逻辑
     *
     * **************************************************************************************
     * */

    @Note("判断 suffix 是否为空，是则返回空字符串，否则就在它的首位之前拼接分隔符")
    private static String private_handleStringKeyPrefix(String loginUserType) {
        if(loginUserType == null || loginUserType.isEmpty()) return "";
        return KEY_SEPARATOR + loginUserType;
    }


    @Note("放行请求到下一步")
    private boolean private_allowGo(HttpServletRequest request, HttpServletResponse response,
                                    String encryptedToken, AuthUser authUser, Object handler) throws Exception {
        this.tokenIn.setData(request, response, this.tokenName, encryptedToken);
        WebKit.openHeader(response, this.tokenName);
        request.setAttribute(AUTH_USER_NAME, authUser);
        return accessManager == null || accessManager.preAuthorize(request, response, handler);
    }

    @Note("初始化授权管理器类，开启授权功能")
    private AccessManager private_buildAccessManager(AuthSource source) {
        //在认证管理器里开启授权功能
        String accessManagerClassName = source.getAccessManagerClassName();
        if(accessManagerClassName == null || accessManagerClassName.isEmpty()) return null;
        AccessSource accessSource = source.getAccessSource();
        if(AuthorizeManager.class.getName().equals(accessManagerClassName)) {
            return new AuthorizeManager(accessSource);
        }
        if(DefaultAccessManager.class.getName().equals(accessManagerClassName)) {
            return  new DefaultAccessManager(accessSource);
        }
        throw new DataGenerationException("您已在认证管理器里开启了授权功能，但是暂不支持 " + accessManagerClassName
                + " 授权管理器类");
    }


    @Note("续期或者放行的操作")
    private boolean private_extendAuthTokenLifeOrBlockForward(GetAuthUserResult result, Object handler,
                                                              HttpServletRequest request, HttpServletResponse response,
                                                              String encryptedToken, AuthToken decryptedAuthToken,
                                                              String key, Logger logger) throws Exception {

        //获取的用户认证对象不为 null，直接放行
        if(!result.authUserIsNull()) {
            return this.private_allowGo(request, response, encryptedToken,
                    result.getAuthUser(), handler);
        }
        //否则就判断是否可以续期，不可以就返回错误信息
        //同时判断 AuthUserService 接口实例是否为 null，若为 null 也不允许续期
        //最后再判断 AuthTokenExtender 接口实例是否也为 null 或者配置参数不符合开启的条件
        if(!result.isCanExtend() || this.authUserService == null
                || this.authTokenExtender == null
                || !this.authTokenExtender.isEnable()) {
            response.getWriter().write(this.tokenExpiredErrorJson);
            return false;
        }
        //续期操作
        boolean extendResult = this.authTokenExtender.extend(request, response, this,
                logger, encryptedToken,
                decryptedAuthToken, this.getTimeunitUsedBySubclass());
        //如果没续成功那就直接过期
        if(!extendResult) {
            response.getWriter().write(this.tokenExpiredErrorJson);
            return false;
        }
        //加载新的 AuthUser 实例然后更新缓存，最后加密续期后的 AuthToken，放入响应里，放行去执行业务
        String loginDeviceType = decryptedAuthToken.getLoginDeviceType();
        AuthUser newAuthUser = this.authUserService.loadAuthUser(decryptedAuthToken.getUserId(),
                decryptedAuthToken.getLoginUserType(), loginDeviceType);
        newAuthUser.setLoginDeviceType(loginDeviceType);
        String newEncryptedToken = decryptedAuthToken.encrypt();
        this.setAuthUser(key, newAuthUser, decryptedAuthToken);
        return this.private_allowGo(request, response, newEncryptedToken,
                newAuthUser, handler);
    }
}
